M2.879 · TFM · Área 2 · EDA

2022-1 · Máster universitario en Ciencia de datos (Data science)

Estudios de Informática, Multimedia y Telecomunicación

 
Nombre y apellidos: Patricia Lázaro Tello

Exploratory Data Analysis (zerowaste y ResortIt)¶

En este notebook se realiza un análisis exploratorio inicial (EDA) del conjunto de datos zerowaste y ResortIt. Estos datasets contienen imágenes de papel y plástico en el contexto deseado (un conveyor belt). Las imágenes pueden contener uno o varios objetos. En el caso de ResortIt, los objetos son sintéticos.

Importación de librerías y setup inicial¶

In [1]:
from pathlib import Path
import os

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

import json

from waste_detection_system import shared_data as base, utils
In [2]:
# plot style
# ==============================================================================
plt.rcParams['axes.titlesize'] = 12
plt.rcParams['figure.titlesize'] = 16
In [3]:
utils.clean_datasets()

Zero Waste (zerowaste-f)¶

Se trata de un conjunto de datos en el contexto ideal (imágenes capturadas con una cámara encima de una cinta donde van pasando los residuos), con una o varias anotaciones por imagen. Las categorías reflejadas en este conjunto de datos son:

  • Papel: 66.31%
  • Plástico: 32.25%
  • Metal: 01.42%

Las anotaciones se encuentran en formato COCO.

In [4]:
jsons = {}

for dirpath, dirs, filenames in os.walk(base.ZERO_WASTE):
  full_path = [os.sep.join([dirpath, filename]) for filename in filenames
               if filename.endswith('.json')]
  for _path in full_path:
    with open(_path, 'r') as _file:
      jsons[dirpath] = json.load(_file)
In [5]:
partitions = {'test', 'train', 'val'}
images = {
    'name' : [],
    'path' : [],
    'width' : [],
    'height' : [],
    'type' : [],
    'label' : [],
    'bbox-x' : [],
    'bbox-y' : [],
    'bbox-w' : [],
    'bbox-h' : [],
}

for _path, json_file in jsons.items():
  partition_type = [part for part in partitions if part in _path][0]
  for image in json_file['images']:
    anns = [item for item in json_file['annotations'] 
           if item['image_id'] == image['id']]

    for ann in anns:
      cat = [item for item in json_file['categories']
            if item['id'] == ann['category_id']][0]
      images['name'] = images['name'] + [image['file_name']]
      images['path'] = images['path'] + [str(Path(_path) / 'data' / image['file_name'])]
      images['width'] = images['width'] + [image['width']]
      images['height'] = images['height'] + [image['height']]
      images['type'] = images['type'] + [partition_type]
      images['label'] = images['label'] + [cat['name']]
      bbox = ann['bbox']
      images['bbox-x'] = images['bbox-x'] + [bbox[0]]
      images['bbox-y'] = images['bbox-y'] + [bbox[1]]
      images['bbox-w'] = images['bbox-w'] + [bbox[2]]
      images['bbox-h'] = images['bbox-h'] + [bbox[3]]

images_df = pd.DataFrame(images)
images_df['label'] = [base.RELATION_CATS[label.upper()] for label in images_df['label']]
In [6]:
images_df.head(n=10)
Out[6]:
name path width height type label bbox-x bbox-y bbox-w bbox-h
0 05_frame_000001.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PLASTICO 1737.3 26.3 182.7 153.1
1 05_frame_000001.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PLASTICO 1024.7 399.4 311.7 443.3
2 05_frame_000001.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PLASTICO 554.2 2.5 525.4 492.9
3 05_frame_000001.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test METAL 1416.6 408.2 192.2 141.3
4 05_frame_000001.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PLASTICO 1120.0 208.6 310.7 253.2
5 05_frame_000001.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PAPEL 597.5 861.2 223.9 123.4
6 05_frame_000001.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PAPEL 189.2 228.4 374.7 314.2
7 05_frame_000011.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PLASTICO 1864.5 20.4 55.5 147.6
8 05_frame_000011.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PLASTICO 1167.3 402.6 296.4 439.8
9 05_frame_000011.PNG raw-datasets\zero-waste\zerowaste-f\test\data\... 1920 1080 test PLASTICO 629.5 1.3 594.2 492.1
In [7]:
len(images_df.index)
Out[7]:
26766
In [8]:
images_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 26766 entries, 0 to 26765
Data columns (total 10 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   name    26766 non-null  object 
 1   path    26766 non-null  object 
 2   width   26766 non-null  int64  
 3   height  26766 non-null  int64  
 4   type    26766 non-null  object 
 5   label   26766 non-null  object 
 6   bbox-x  26766 non-null  float64
 7   bbox-y  26766 non-null  float64
 8   bbox-w  26766 non-null  float64
 9   bbox-h  26766 non-null  float64
dtypes: float64(4), int64(2), object(4)
memory usage: 2.0+ MB
In [9]:
images_df['type'].value_counts()
Out[9]:
train    18002
test      5077
val       3687
Name: type, dtype: int64
In [10]:
images_df['type'].value_counts(normalize=True)
Out[10]:
train    0.672570
test     0.189681
val      0.137749
Name: type, dtype: float64
In [11]:
images_df['label'].value_counts(normalize=True)
Out[11]:
PAPEL       0.663192
PLASTICO    0.322536
METAL       0.014272
Name: label, dtype: float64
In [12]:
images_df['label'].value_counts(normalize=False)
Out[12]:
PAPEL       17751
PLASTICO     8633
METAL         382
Name: label, dtype: int64
In [13]:
images_df['width'].value_counts(normalize=True)
Out[13]:
1920    1.0
Name: width, dtype: float64
In [14]:
images_df['height'].value_counts(normalize=True)
Out[14]:
1080    1.0
Name: height, dtype: float64
In [15]:
sample_imgs = images_df[(images_df.type == 'train')].sample(n=3)
utils.plot_data_sample(sample_imgs, images_df)
In [16]:
with open(base.ZERO_WASTE_CSV, 'w', encoding='utf-8-sig') as f:
  images_df.to_csv(f, index=False)
In [17]:
images_df['path'].map(lambda p: Path(p).suffix).value_counts()
Out[17]:
.PNG    26766
Name: path, dtype: int64

ResortIt¶

Se trata de un conjunto de datos artificial, creado a partir de recortes de residuos de plástico tomados en vista aérea superpuestos sobre diferentes fondos, con una o varias anotaciones por imagen. Las categorías reflejadas en este conjunto de datos son:

  • Papel: 25%
  • Plástico: 50%
  • Metal: 25%

Las anotaciones se encuentran en formato COCO.

In [18]:
jsons = {}

for dirpath, dirs, filenames in os.walk(base.RESORTIT):
  full_path = [os.sep.join([dirpath, filename]) for filename in filenames
               if filename.endswith('.json')]
  for _path in full_path:
    with open(_path, 'r') as _file:
      jsons[_path] = json.load(_file)
In [19]:
partitions = {'test', 'train', 'val'}
images = {
    'name' : [],
    'path' : [],
    'width' : [],
    'height' : [],
    'type' : [],
    'label' : [],
    'bbox-x' : [],
    'bbox-y' : [],
    'bbox-w' : [],
    'bbox-h' : [],
}

for _path, json_file in jsons.items():
  partition_type = [part for part in partitions if part in _path][0]
  for image in json_file['images']:
    anns = [item for item in json_file['annotations'] 
           if item['image_id'] == image['id']]

    for ann in anns:
      cat = [item for item in json_file['categories']
            if item['id'] == ann['category_id']][0]
      images['name'] = images['name'] + [image['file_name']]
      images['path'] = images['path'] + [str(Path(_path).parents[1] / partition_type / image['file_name'])]
      images['width'] = images['width'] + [image['width']]
      images['height'] = images['height'] + [image['height']]
      images['type'] = images['type'] + [partition_type]
      images['label'] = images['label'] + [cat['name']]
      bbox = ann['bbox']
      images['bbox-x'] = images['bbox-x'] + [bbox[0]]
      images['bbox-y'] = images['bbox-y'] + [bbox[1]]
      images['bbox-w'] = images['bbox-w'] + [bbox[2]]
      images['bbox-h'] = images['bbox-h'] + [bbox[3]]

images_df = pd.DataFrame(images)
images_df['label'] = [base.RELATION_CATS[label.upper()] for label in images_df['label']]
In [20]:
images_df.head(n=10)
Out[20]:
name path width height type label bbox-x bbox-y bbox-w bbox-h
0 20000.jpg raw-datasets\ResortIt\train\20000.jpg 800 800 train METAL 330 337 136 172
1 20000.jpg raw-datasets\ResortIt\train\20000.jpg 800 800 train METAL 256 278 126 172
2 19999.jpg raw-datasets\ResortIt\train\19999.jpg 800 800 train METAL 376 376 128 177
3 19999.jpg raw-datasets\ResortIt\train\19999.jpg 800 800 train METAL 307 294 137 169
4 19998.jpg raw-datasets\ResortIt\train\19998.jpg 800 800 train METAL 226 341 49 50
5 19998.jpg raw-datasets\ResortIt\train\19998.jpg 800 800 train METAL 240 360 119 134
6 19997.jpg raw-datasets\ResortIt\train\19997.jpg 800 800 train METAL 307 278 132 155
7 19997.jpg raw-datasets\ResortIt\train\19997.jpg 800 800 train METAL 362 138 162 162
8 19996.jpg raw-datasets\ResortIt\train\19996.jpg 800 800 train METAL 388 127 172 104
9 19996.jpg raw-datasets\ResortIt\train\19996.jpg 800 800 train METAL 187 281 21 56
In [21]:
len(images_df.index)
Out[21]:
43200
In [22]:
images_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 43200 entries, 0 to 43199
Data columns (total 10 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   name    43200 non-null  object
 1   path    43200 non-null  object
 2   width   43200 non-null  int64 
 3   height  43200 non-null  int64 
 4   type    43200 non-null  object
 5   label   43200 non-null  object
 6   bbox-x  43200 non-null  int64 
 7   bbox-y  43200 non-null  int64 
 8   bbox-w  43200 non-null  int64 
 9   bbox-h  43200 non-null  int64 
dtypes: int64(6), object(4)
memory usage: 3.3+ MB
In [23]:
images_df['type'].value_counts()
Out[23]:
train    32000
val      11200
Name: type, dtype: int64
In [24]:
images_df['type'].value_counts(normalize=True)
Out[24]:
train    0.740741
val      0.259259
Name: type, dtype: float64
In [25]:
images_df['label'].value_counts(normalize=True)
Out[25]:
PLASTICO    0.50
METAL       0.25
PAPEL       0.25
Name: label, dtype: float64
In [26]:
images_df['label'].value_counts(normalize=False)
Out[26]:
PLASTICO    21600
METAL       10800
PAPEL       10800
Name: label, dtype: int64
In [27]:
images_df['width'].value_counts(normalize=True)
Out[27]:
800    1.0
Name: width, dtype: float64
In [28]:
images_df['height'].value_counts(normalize=True)
Out[28]:
800    1.0
Name: height, dtype: float64
In [29]:
sample_imgs = images_df[(images_df.type == 'train')].sample(n=3)
utils.plot_data_sample(sample_imgs, images_df)
In [30]:
with open(base.RESORTIT_CSV, 'w', encoding='utf-8-sig') as f:
  images_df.to_csv(f, index=False)
In [31]:
images_df['path'].map(lambda p: Path(p).suffix).value_counts()
Out[31]:
.jpg    43200
Name: path, dtype: int64

Creación del dataset final¶

In [32]:
with open(base.FINAL_DATA_CSV, 'w', encoding='utf-8-sig') as final_file,\
  open(base.ZERO_WASTE_CSV, 'r',  encoding='utf-8-sig') as zerowaste_file,\
  open(base.RESORTIT_CSV, 'r', encoding='utf-8-sig') as resortit_file:

  zerowaste = pd.read_csv(zerowaste_file)
  zerowaste['dataset'] = 'final'
  zerowaste = zerowaste.apply(lambda i : utils.batch_conversion_to_jpg(i, resize=False, 
                                   dataset_type=utils.DATASET_TYPES.FINAL), axis=1)

  resortit = pd.read_csv(resortit_file)
  resortit['dataset'] = 'complementary'
  resortit = resortit.apply(lambda i : utils.batch_conversion_to_jpg(i, resize=False, 
                                    dataset_type=utils.DATASET_TYPES.COMPLEMENTARY), axis=1)

  images_df = pd.concat([zerowaste, resortit])
  images_df.to_csv(final_file, index=False)
In [33]:
fig, axes = plt.subplots(ncols=2, nrows=2, figsize=(15,10), dpi=300, sharey=True)
axes = axes.flatten()
fig.suptitle('Estadísticas del dataset', fontsize=20)
sns.set_theme()

zw_label_plot = axes[0]
ri_label_plot = axes[1]
final_label_plot = axes[2]
type_plot = axes[3]


zw_filter = images_df['dataset']=='final'
zw = images_df[zw_filter]
zw_label_info = zw['label'].value_counts(normalize=True)
zw_label_plot = sns.barplot(ax=zw_label_plot, data=zw_label_info.reset_index(), x='index', y='label', palette='rocket', 
    hue='label', dodge=False)
for container in zw_label_plot.containers:
    zw_label_plot.bar_label(container, fmt='%.2f')
zw_label_plot.get_legend().remove()
zw_label_plot.set(xlabel='etiqueta', ylabel='', ylim=(0.0, 1.0))
zw_label_plot.set_title('ZeroWaste')


ri_filter = images_df['dataset']=='complementary'
ri = images_df[ri_filter]
ri_label_info = ri['label'].value_counts(normalize=True)
ri_label_plot = sns.barplot(ax=ri_label_plot, data=ri_label_info.reset_index(), x='index', y='label', palette='rocket', 
    hue='label', dodge=False)
for container in ri_label_plot.containers:
    ri_label_plot.bar_label(container, fmt='%.2f')
ri_label_plot.get_legend().remove()
ri_label_plot.set(xlabel='etiqueta', ylabel='', ylim=(0.0, 1.0))
ri_label_plot.set_title('ResortIt')


label_info = images_df['label'].value_counts(normalize=True)
type_info = images_df['type'].value_counts(normalize=True)

final_label_plot = sns.barplot(ax=final_label_plot, data=label_info.reset_index(), x='index', y='label', palette='rocket', 
    hue='label', dodge=False)
for container in final_label_plot.containers:
    final_label_plot.bar_label(container, fmt='%.2f')
final_label_plot.get_legend().remove()
final_label_plot.set(xlabel='etiqueta', ylabel='', ylim=(0.0, 1.0))
final_label_plot.set_title('Distribución conjunta de clases')

type_plot = sns.barplot(ax=type_plot, data=type_info.reset_index(), x='index', y='type', palette='rocket', 
    hue='type', dodge=False)
for container in type_plot.containers:
    type_plot.bar_label(container, fmt='%.2f')
type_plot.get_legend().remove()
type_plot.set(xlabel='tipo', ylabel='', ylim=(0.0, 1.0))
type_plot.set_title('Distribución en train/val/test')

fig.tight_layout()
plt.show()

Se observa la distribución de observaciones por etiqueta:

PAPEL PLÁSTICO METAL VIDRIO ORGÁNICO OTROS TOTAL
% Zero Waste 63.32% 32.25% 1.43% 0.0% 0.0% 0.0% 100%
% ResortIt 25% 50% 25% 0.0% 0.0% 0.0% 100%
Nº observaciones ZW 17.751 8.633 382 0 0 0 26.766
Nº observaciones RI 10.800 21.600 10.800 0 0 0 43.200

El conjunto de datos ZeroWaste no está balanceado: papel y plástico ocupan más del 95% del conjunto de datos, mientras que solo hay 382 observaciones de metal y no están disponibles anotaciones de vidrio, residuos orgánicos y otros residuos no reciclables. Respecto a ResortIt, muestra una distribución algo menos desbalanceada que ZeroWaste, con el doble de plástico que de papel y metal. Tampoco contiene vidrio, orgánico u otros residuos no reciclables.

Este dataset no representa la distribución de datos real que se va a encontrar en el ámbito de aplicación: recuperación de residuos reciclables de la fracción resto, debido a las siguientes razones:

  • Existen poblaciones en que no se separa el residuo orgánico (contenedor marrón) de la fracción resto (contenedor gris).
  • Aunque la cultura del reciclaje está más extendida que hace unos años, todavía hay personas que no reciclan y por tanto, se ha de suponer que la fracción resto debería contener residuos de todos los tipos.
  • La distribución de observaciones en las clases no está balanceada y muestra una clara tendencia hacia el papel y el plástico.
Al margen de este notebook se ha realizado la exploración y análisis de conjuntos de datos complementarios que pudieran rellenar los huecos presentes en el dataset inicial. Estos conjuntos de datos son:
  • Cigbutts: https://www.immersivelimit.com/datasets/cigarette-butts
  • Drinking Waste: https://paperswithcode.com/dataset/drinking-waste-classification
  • TACO: https://www.kaggle.com/datasets/kneroma/tacotrashdataset

La unión de estos cuatro conjuntos de datos produce la siguiente distirbución de clases:

PAPEL PLASTICO VIDRIO METAL ORGANICO OTROS TOTAL
% 47.55% 39.02% 3.53% 0.99% 0.02% 8.87% 100%
Nº observaciones 18.338 15.049 1.362 382 8 3.422 38.561
Para completar y balancear el conjunto de datos, se han utilizado otros datasets utilizados en tareas de clasificación, anotándolos parcialmente mediante la herramienta http://makesense.ai. Después se han aplicado técnicas de aprendizaje débilmente supervisado, utilizando una red Faster R-CNN inicializada con los pesos del dataset de COCO 2017; sin embargo, no se ha podido obtener información útil de estos conjuntos de datos.
  • Waste Classification: https://www.kaggle.com/datasets/techsash/waste-classification-data
  • Trashbox (metal): https://github.com/nikhilvenkatkumsetty/TrashBox
  • CompostNet: https://github.com/sarahmfrost/compostnet
In [34]:
images_df = images_df[images_df.label.isin([base.CATS_PAPEL, base.CATS_PLASTICO])]
images_df['label'].value_counts()
Out[34]:
PLASTICO    30233
PAPEL       28551
Name: label, dtype: int64
In [35]:
with open(base.FINAL_DATA_CSV, 'w', encoding='utf-8-sig') as f:
  images_df.to_csv(f, index=False)
In [36]:
fig, axes = plt.subplots(ncols=2, nrows=1, figsize=(15,5), dpi=300, sharey=True)
fig.suptitle('Estadísticas del dataset', fontsize=20)
sns.set_theme()

label_plot = axes[0]
type_plot = axes[1]

label_info = images_df['label'].value_counts(normalize=True)
type_info = images_df['type'].value_counts(normalize=True)

label_plot = sns.barplot(ax=label_plot, data=label_info.reset_index(), x='index', y='label', palette='rocket', 
    hue='label', dodge=False)
for container in label_plot.containers:
    label_plot.bar_label(container, fmt='%.2f')
label_plot.get_legend().remove()
label_plot.set(xlabel='etiqueta', ylabel='', ylim=(0.0, 1.0))

type_plot = sns.barplot(ax=type_plot, data=type_info.reset_index(), x='index', y='type', palette='rocket', 
    hue='type', dodge=False)
for container in type_plot.containers:
    type_plot.bar_label(container, fmt='%.2f')
type_plot.get_legend().remove()
type_plot.set(xlabel='tipo', ylabel='', ylim=(0.0, 1.0))

fig.tight_layout()
plt.show()

La distribución final de las clases es la siguiente:

PAPEL PLÁSTICO TOTAL
% 51% 49% 100%
Nº observaciones 30.233 28.551 58.784

Finalmente se ha decidido descartar la creación de un conjunto de datos completo y balanceado debido a que los nuevos datos estaban fuera de contexto (Garbage In, Garbage Out) y no solucionaban el problema de desbalanceo de clases. Así mismo, se ha decidido acotar el ámbito del estudio a la detección y clasificación de papel y plástico (se descarta la categoría metal por no tener una muestra significativa en el dataset final ZeroWaste).

La justificación detrás de esta decisión recae en los medios utilizados actualmente para la segregación de residuos, que se basan en combinaciones de fuerzas mecánicas y magnéticas, entre otras. De esta forma, el papel y el plástico ligero (envoltorios) son separados de materiales más pesados como pueden ser el vidrio, plástico duro y material orgánico.

Debido a que el papel y el plástico ligero comparten características, su segregación mediante fuerzas mecánicas es más complicada. El objetivo de este proyecto consiste en la creación de un sistema de detección de papel y plástico a través de una cámara montada encima de una cinta transportadora de residuos. Futuras líneas de investigación incluirían la creación de un sistema robótico de segregación o la creación de un dataset más completo que permitiera realizar un trabajo más a fondo en la separación de residuos reciclables de la fracción resto.